﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Muel.TypeScriptFacade.Core
{
    public static class TypeScriptGenerator
    {
        public const string UtilitiesModule = @"
    export module Utilities {
        export class Arr {
            public static Equals(first: Array<any>, second: Array<any>): boolean {
                // if the other array is a falsy value, return
                if (!second)
                    return false;

                // compare lengths - can save a lot of time 
                if (first.length != second.length)
                    return false;

                for (var i = 0, l = first.length; i < l; i++) {
                    // Check if we have nested arrays
                    if (first[i] instanceof Array && second[i] instanceof Array) {
                        // recurse into the nested arrays
                        if (!this.Equals(first[i], second[i]))
                            return false;
                    }
                    else if (first[i] != second[i]) {
                        // Warning - two different object instances will never be equal: {x:20} != {x:20}
                        return false;
                    }
                }
                return true;
            }
        }
    }";

        /// <summary>
        /// {0}: type register
        /// {1}: auto-snapshot code
        /// {2}: auto-snapshot code
        /// </summary>
        private const string CoreModuleFormat = @"
    export module Core {{
        class AjaxCall {{
            private xhr: XMLHttpRequest;
            constructor() {{
                var xhr: any;
                if (typeof XMLHttpRequest !== 'undefined') {{
                    xhr = new XMLHttpRequest();
                }}
                else {{
                    var versions = [
                        ""MSXML2.XmlHttp.5.0"",
                        ""MSXML2.XmlHttp.4.0"",
                        ""MSXML2.XmlHttp.3.0"",
                        ""MSXML2.XmlHttp.2.0"",
                        ""Microsoft.XmlHttp""
                    ];
                    for (var i = 0; i < versions.length; i++) {{
                        try {{
                            xhr = new ActiveXObject(versions[i]);
                            break;
                        }}
                        catch (e) {{
                            // swallow the exception
                        }}
                    }}
                }}
                this.xhr = xhr;
            }}

            private send(url: string, callback: (success: boolean, responseData: string) => any, method: string, data: string) {{
                this.xhr.open(method, url, true);
                this.xhr.onreadystatechange = () => {{
                    if (this.xhr.readyState == XMLHttpRequest.DONE) {{
                        var success = false;
                        if (this.xhr.status == 200) {{
                            success = true;
                        }}
                        callback(success, this.xhr.responseText);
                    }}
                }};
                if (method == 'POST') {{
                    this.xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
                }}
                this.xhr.send(data);
            }}

            public post(url: string, data: any, callback: (success: boolean, responseData: string) => any) {{
                var query: string = JSON.stringify(data);
                this.send(url, callback, 'POST', query);
            }}

            public dispose() {{
                this.xhr = null;
            }}
        }}
  
        export class TypeRegister {{
            private static types: {{ [idx: string]: any }};
            public static Get(type: string): any {{
                if (this.types == null) {{
                    this.types = {{
{0}
                    }};
                }}
                if (typeof (this.types[type]) == 'function') {{
                    return this.types[type];
                }}
                return null;
            }}

            // this function will generate concrete objects based on the returned objects
            // to ensure that abstract base return types are instantiated with their relevant
            // concrete classes
            public static Instantiate(obj: any, fallbackBaseType: any): any {{

                var returnObj: any;

                if (typeof(obj['__type']) !== 'undefined') {{

                    // the object has a type added by .NET service layer
                    var c = this.Get(obj['__type']);
                    returnObj = new c(obj);
                    {1}
                }}
                else if (obj.constructor === Array) {{

                    // the instantiation is on an array
                    returnObj = [];
                    obj = <Array<any>>obj;
                    for (var i = 0; i < obj.length; i++) {{
                        returnObj.push(this.Instantiate(obj[i], fallbackBaseType));
                    }}
                }}
                else if (fallbackBaseType != null) {{

                    // if we've reached this stage we're going to rely on the fallback type
                    returnObj = new fallbackBaseType(obj);
                    {2}
                }}
                else {{

                    // the object does not have a type returned by .NET service layer, and it
                    // hasn't got a fallback base type specified with it
                    // doesn't appear to be an array, so assume a value type and return it raw
                    returnObj = obj;

                }}

                return returnObj;
            }}
        }}

        export class Service {{
            static Call(endPoint: string, postData: any, responseHandler: (returnObject: any) => void, fallbackBaseType: any): void {{
                var call = new AjaxCall();
                call.post(endPoint, postData, (success: boolean, responseData: string) => {{
                    call.dispose();
                    if (success) {{
                        if (responseData == null || responseData == '') {{
                            responseHandler(null);
                            return;
                        }}
                        var response = JSON.parse(responseData);
                        var obj;
                        if (typeof response.d !== 'undefined') {{ obj = response.d; }}
                        else {{ obj = response; }}
                        var returnObj = TypeRegister.Instantiate(obj, fallbackBaseType);
                        responseHandler(returnObj);
                    }}
                    else {{
                        throw Error(""Service call failed with error: "" + responseData);
                    }}
                }});
            }}
        }}
    }}
";
        private const string TypeRegisterItemFormat = @"
                        ""{0}"" : {1}";

        private const string ServiceFacadeFormat = @"
module {0} {{
    {1}
    {2}
    {3}
}}";

        internal static string Create(TypeScriptServiceManager manager)
        {
            // create the service code
            string serviceCode = string.Join(Environment.NewLine, manager.Services.Select(svc => svc.ToTypeScript()));

            // create the core module code - include auto-snapshot if necessary
            string autoSnapshotCode = (manager.Settings.ChangeTracking.Enabled && manager.Settings.ChangeTracking.AutoSnapshotReturnedObjects)
                ?
                string.Format("returnObj.{0}();", manager.Settings.ChangeTracking.SnapshotMethodName)
                :
                string.Empty;

            // create the type register to help instantiate the correct concrete class
            // when the return type is a base class
            string typeRegisterCode = string.Join(",",
                manager.ObjectManager.cache.Values
                    .OfType<TypeScriptClass>()
                    .Where(obj => obj.ShouldEmit && !obj.IsAbstract)
                    .Select(obj => string.Format(TypeRegisterItemFormat, obj.GetTypeProperty(), obj.GetTypeScriptType())).ToArray());

            string coreModuleCode = string.Format(TypeScriptGenerator.CoreModuleFormat, typeRegisterCode, autoSnapshotCode, autoSnapshotCode);
            
            // first output the plumbing and the service code to create the ServiceFacade module
            string output = string.Format(ServiceFacadeFormat,
                manager.Settings.ServiceFacadeName,
                TypeScriptGenerator.UtilitiesModule,
                coreModuleCode,
                serviceCode
                );
            
            // then generate the objects that are used in the services
            output += manager.ObjectManager.ToTypeScript();
            
            // finally clean it for output (removes single line comments and whitespace-only lines)
            // note we are not minifying, to ensure code is still readable - minification can happen 
            // on the JavaScript, post-TypeScriptCompile
            return output.Clean();
        }
    }
}
